Skip to content

Add call-site constructor cache with zero-arg leaf fast path for new Date()#2624

Merged
lahma merged 3 commits into
sebastienros:mainfrom
lahma:perf-new-date-cache
Jul 10, 2026
Merged

Add call-site constructor cache with zero-arg leaf fast path for new Date()#2624
lahma merged 3 commits into
sebastienros:mainfrom
lahma:perf-new-date-cache

Conversation

@lahma

@lahma lahma commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Every new Date() re-tested IsConstructor and entered the generic Engine.Construct — call-stack push/pop, recursion-depth compare — although the resolved constructor is already version-validated by the global identifier cache and its verdicts are immutable per instance. The stopwatch row pays this 171k times per op.

JintNewExpression now remembers the last constructor object it constructed through (monomorphic, ReferenceEquals-validated — identity pins the realm and the per-instance verdicts):

  • An identity hit skips the IsConstructor type-test.
  • When the cached constructor is additionally a zero-argument leaf built-in — a new internal virtual Function.IsZeroArgLeafConstructor, overridden by DateConstructor only when the engine runs the exact stock DefaultTimeSystem — the construct bypasses Engine.Construct entirely. Such a constructor runs no user-observable code and cannot raise a JavaScript error, so the call-stack frame is dead weight. Derived or custom time systems keep the frame (a throwing GetUtcNow would otherwise lose its new Date() stack entry), as do debug-mode engines and suspendable frames; LastSyntaxElement is still set so a .NET-level failure resolves its script location.
  • A miss (callee reassigned, another engine sharing the prepared node) falls through to the generic path and re-caches — never a permanent decline (the Re-resolve slot locations when a shared handler tree changes function instance #2616 pattern).

Scope is deliberately Date-first: the mechanism is general, but Error captures the stack (never leaf) and other built-ins aren't exercised by any row today.

Six pins in NewExpressionCacheTests: realm-prototype identity over a warmed call site, mid-script Date reassignment picked up at a warm site, subclass newTarget prototypes, two engines alternating over one shared prepared script, custom-ITimeSystem clocks served through the generic path, and a throwing custom time system surfacing its exception per construct.

Benchmarks (before/after, same machine, sequential)

Row main PR Δ
DateConstruction NewDateNow (100k constructs/op) 8.393 ms 7.037 ms −16.2% (84 → 70 ns/construct)
DateConstruction NewDateMillis (guard: 1-arg, non-leaf) 13.570 ms 12.967 ms −4.4%

Stopwatch rows moved within their single-launch mode bands in both directions (classic Execute −1.1%, classic ParsedScript +5.5%, modern flat; the mechanism's expected row share is only ~2%, inside the bands — the isolated driver row above is the evidence; row certification comes with the next EngineComparison refresh). PreparedAnomaly rows: times ±3..7% mixed-sign, allocations +10–20 B/op from the two new fields on JintNewExpression handler instances (fresh-engine-per-op hosts rebuild handlers each op) — disclosed, negligible.

Gates: Jint.Tests 3,295+3,232 pass (6 new pins), PublicInterface 82×2*, Test262 99,426 pass / 0 fail (exact baseline).

* the net472 TimeSystemTests.CanUseTimeProvider wall-clock-tolerance test currently fails on this machine on unmodified main as well (verified 3/3, ~7 ms past its 100 ms window, post-sleep timer state) — environmental, not attributable to the change; the net10 arm and all other net472 tests pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv

lahma and others added 2 commits July 10, 2026 20:29
…Date()

Every new Date() re-tested IsConstructor and entered the generic
Engine.Construct with its call-stack push/pop and recursion-depth
compare - 171k times per stopwatch benchmark op - although the resolved
constructor is version-validated by the global identifier cache and its
verdicts are immutable per instance.

JintNewExpression now remembers the last constructor object it
constructed through. An identity hit skips the IsConstructor test; when
the cached constructor is additionally a zero-argument leaf built-in
(new virtual Function.IsZeroArgLeafConstructor, overridden by
DateConstructor when the engine runs the exact stock DefaultTimeSystem),
the construct bypasses Engine.Construct entirely - such a constructor
runs no user-observable code and cannot raise a JavaScript error, so the
call-stack frame is dead. Custom or derived time systems keep the frame
(a throwing GetUtcNow would otherwise lose its new Date() stack entry),
as do debug-mode engines and suspendable frames. A cache miss (callee
reassigned, another engine sharing the prepared node) falls through to
the generic path and re-caches - never a permanent decline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv
@lahma
lahma enabled auto-merge (squash) July 10, 2026 18:36
@lahma
lahma merged commit 86ca69e into sebastienros:main Jul 10, 2026
4 checks passed
@lahma
lahma deleted the perf-new-date-cache branch July 10, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant